#!/usr/bin/env python
# -*- coding: utf-8 -*-

import uuid
import datetime

from farado.items.issue import Issue
from farado.items.file import File
from farado.items.comment import Comment
from farado.items.field_kind import ValueTypes
from farado.permission_manager import PermissionFlag
from farado.general_manager_holder import project_manager
from farado.editors.editor_result_code import EditResultCode
from farado.ui.base_view import get_value, get_int_value, get_values
from farado.helpers.changes_helper import IssueChangesHelper
from farado.helpers.changes_user_notifier import ChangesUserNotifier


class IssueEditor():
    def __init__(self, user) -> None:
        self.changes_helper = IssueChangesHelper(user.id)
        self.changes_notifier = ChangesUserNotifier(user.id)

    #--------------------------------------------------------------------------#
    def change(self, rights, issue_id=None, **args):
        '''Вносит изменения в БД касательно целевого запроса (issue). Изменяет
        существующий объект или создаёт новый.

        Параметры
        ----------
        rights : PermissionFlags
            Число, сформированное из перечисления (IntEnum) прав PermissionFlag.
            Содержит информацию о том, какие права есть у активного пользователя
            на данный объект.

        issue_id : int
            Идентификатор целевого запроса.

        args : dict
            Словарь с данными, которые необходимо применить к целевому запроса.

        Возвращает
        ----------
        result_code : EditResultCode
            Элемент перечисления, несущий информацию об успехе и типе операции.

        result_text : string
            Текстовая строка с информацией для конечного пользователя
            о результатах применения изменений в БД.

        target_issue_id : int
            Идентификатор целевого запроса, используется при создании нового
            экземпляра запроса.
        '''
        if PermissionFlag.editor > rights:
            return (EditResultCode.none, 'Нет доступа к изменению запроса', False)

        issue = project_manager().issue(issue_id)
        need_create_new = bool(not issue)

        if need_create_new:
            if PermissionFlag.creator > rights:
                return (EditResultCode.none, 'Нет доступа к созданию нового запроса', False)

            issue_kind_id = args['issue_kind_id'] if 'issue_kind_id' in args else None
            issue = project_manager().create_issue(issue_kind_id)
            if not issue:
                return (EditResultCode.none, 'Тип запроса не найден', False)

            self.changes_helper.follow(issue)
            if 'issue_files_editor' in args:
                project_manager().save_item(issue)

                issue_files = args['issue_files_editor']
                if not list == type(issue_files) and issue_files:
                    issue_files = [issue_files]
                for file_data in issue_files:
                    if not file_data or not file_data.file:
                        continue

                    # FIXME : Разобраться с проблемой при перенаправлении запроса приходит строка
                    if str == type(file_data):
                        continue

                    file_id = str(uuid.uuid4())
                    file_path = f'issue_{issue.id}'
                    project_manager().file_manager.save_uploaded_file(
                        file_path,
                        file_id,
                        file_data.file.read(),
                    )

                    # Получение имени файла и преобразование в utf8 для корректной
                    # работы с кириллицей
                    if 'Content-Disposition' in file_data.headers:
                        filename = value_from_string(
                            file_data.headers['Content-Disposition'],
                            "filename").encode("latin1").decode("utf8")
                    else:
                        filename = file_data.filename

                    issue.files.append(File(
                        filename,
                        file_id,
                        file_path,
                    ))
        else:
            self.changes_helper.follow(issue)

        issue.caption = get_value(args, 'issue_caption', issue.caption)
        issue.content = get_value(args, 'issue_content', issue.content)
        issue.project_id = get_int_value(args, 'issue_project_id', issue.project_id)
        issue.state_id = get_int_value(args, 'issue_state_id', issue.state_id)
        issue.version_id = get_int_value(args, 'issue_version_id', issue.version_id)

        # Конструкция нужна чтобы избежать петли в связях дочерних элементов
        parent_id = get_int_value(args, 'issue_parent_id', issue.parent_id)
        if not parent_id == issue.parent_id:
            if not parent_id == issue.id:
                for sub_issue in project_manager().sub_issues_in_deep(issue.id):
                    if parent_id == sub_issue.id:
                        break
                else:
                    issue.parent_id = parent_id

        # Назначения значений пользовательских полей issue
        for field in issue.fields:
            if field_kind := project_manager().field_kind(field.field_kind_id):
                field.set_value(
                    value=get_value(
                        args,
                        f'field_kind_{field.field_kind_id}',
                        field.value,
                    ),
                    value_type=field_kind.value_type,
                )

        project_manager().save_item(issue)
        change = self.changes_helper.finalize(issue)
        self.changes_notifier.notify_by_issue(issue, change)

        if need_create_new:
            return (EditResultCode.created, 'Новый запрос сохранён', issue.id)

        return (EditResultCode.modified, 'Запрос сохранён', issue.id)

    #--------------------------------------------------------------------------#
    def create_issue(self, rights, issue_kind_id, parent_id=None, project_id=None):
        '''Создаёт новый экземпляр запроса для дальнейшего его изменения
        и сохранения в БД.
        '''
        # TODO: проверять issue_kind_rights
        temporary_issue = project_manager().create_issue(issue_kind_id)
        if parent_id:
            temporary_issue.parent_id = int(parent_id)
            if parent_issue := project_manager().issue(parent_id):
                temporary_issue.version_id = parent_issue.version_id

        if project_id:
            temporary_issue.project_id = int(project_id)

        return (EditResultCode.created, "Создан временный запрос", temporary_issue)

    #--------------------------------------------------------------------------#
    def remove_issue(self, rights, issue_id):
        '''Удаляет целевой экземпляр запроса из БД.
        '''
        # TODO: проверять issue_kind_rights

        issue = project_manager().issue(issue_id)
        if not issue:
            return (EditResultCode.none, "Запрос не найден")

        for file in issue.files:
            project_manager().remove_file(file)
        for comment in issue.comments:
            for file in comment.files:
                project_manager().remove_file(file)

        project_manager().remove_item(Issue, issue_id)

        return (EditResultCode.removed, "Запрос удалён")

    #--------------------------------------------------------------------------#
    def add_file(self, rights, issue_id, **args):
        '''Сохраняет файл в папке загрузок и создаёт соответствующий объект в БД.

        Параметры
        ----------
        rights : PermissionFlags
            Число, сформированное из перечисления (IntEnum) прав PermissionFlag.
            Содержит информацию о том, какие права есть у активного пользователя
            на данный объект.

        issue_id : int
            Идентификатор целевого запроса (issue).

        args : dict
            Словарь с данными, которые необходимо оформить в виде файла для
            целевого запроса (issue).

        Возвращает
        ----------
        result_code : EditResultCode
            Элемент перечисления, несущий информацию об успехе и типе операции.

        result_text : string
            Текстовая строка с информацией для конечного пользователя
            о результатах применения изменений в БД.

        reserved : bool
            Зарезервированный параметр для передачи дополнительной информации
            об изменяемых объектах.
        '''
        issue = project_manager().issue(issue_id)
        if not issue:
            return (EditResultCode.none, 'Не найден запрос', False)

        if PermissionFlag.editor > rights:
            return (EditResultCode.none, 'Нет доступа к изменению запроса', False)

        self.changes_helper.follow(issue)
        file_data = args['issue_files_editor']
        if file := save_file_to_storage(issue_id, file_data):
            issue.files.append(file)
        project_manager().save_item(issue)
        self.changes_helper.finalize(issue)

        return (EditResultCode.created, 'Файл добавлен', False)

    #--------------------------------------------------------------------------#
    def remove_file(self, rights, issue_id, file_id, key):
        issue = project_manager().issue(issue_id)
        if not issue:
            return (EditResultCode.none, 'Не найден запрос', False)

        if PermissionFlag.editor > rights:
            return (EditResultCode.none, 'Нет доступа к изменению запроса', False)

        if not file_id:
            file_id = key

        file = issue.file(file_id)
        if not file:
            return (EditResultCode.none, 'Файл не найден', False)

        self.changes_helper.follow(issue)
        project_manager().remove_file(file)

        issue = project_manager().issue(issue_id)
        self.changes_helper.finalize(issue)

        return (EditResultCode.removed, 'Файл удалён', False)

    #--------------------------------------------------------------------------#
    def add_comment(self, rights, issue_id, args, user):
        issue = project_manager().issue(issue_id)
        if not issue:
            return (EditResultCode.none, 'Не найден запрос', False)

        if PermissionFlag.editor > rights:
            return (EditResultCode.none, 'Нет доступа к изменению запроса', False)

        self.changes_helper.follow(issue)

        value = get_value(args, 'new_comment_value', None)
        if not value:
            return (EditResultCode.none, 'Пустое значение комментария недопустимо', False)

        comment = Comment(
            issue_id=int(issue_id),
            user_id=int(user.id),
            creation_datetime=datetime.datetime.now(),
            content=value)

        for file_data in get_values(args, 'new_comment_files'):
            if file := save_file_to_storage(issue_id, file_data):
                comment.files.append(file)

        project_manager().save_item(comment)

        issue = project_manager().issue(issue_id)
        change = self.changes_helper.finalize(issue)
        self.changes_notifier.notify_by_issue(issue, change)

        return (EditResultCode.created, 'Комментарий добавлен', comment.id)

    #--------------------------------------------------------------------------#
    def remove_comment(self, rights, issue_id, comment_id):
        issue = project_manager().issue(issue_id)
        if not issue:
            return (EditResultCode.none, 'Не найден запрос', False)

        if PermissionFlag.editor > rights:
            return (EditResultCode.none, 'Нет доступа к изменению запроса', False)

        comment = issue.comment(comment_id)
        if not comment:
            return (EditResultCode.none, 'Не найден файл', False)

        self.changes_helper.follow(issue)
        for file in comment.files:
            project_manager().remove_file(file)
        project_manager().remove_item(Comment, comment_id)

        issue = project_manager().issue(issue_id)
        change = self.changes_helper.finalize(issue)
        self.changes_notifier.notify_by_issue(issue, change)

        return (EditResultCode.removed, 'Комментарий удалён', False)

#------------------------------------------------------------------------------#
def value_from_string(data, param):
    '''Получает из строки с перечнем параметров значение указанного.

    Параметры
    ----------
    data : str
        Строка, содержащая перечень параметров.
        Пример: 'form-data; name="issue_files_editor"; filename="123.txt"'

    param : str
        Имя параметра, значение которого нужно достать из строки.
        Пример: 'filename'

    Возвращает
    ----------
    value : str
        Строковое значение указанного параметра.
        Пример: '123.txt'
    '''
    param += '="'
    start_index = data.find(param) + len(param)
    if -1 == start_index:
        return ''

    data_part = data[start_index:]
    end_index = data[start_index:].find('"')
    if -1 == end_index:
        return ''

    return data_part[:end_index]


#------------------------------------------------------------------------------#
def save_file_to_storage(issue_id, file_data):
    '''Сохраняет в файловое хранилище файл полученный из multipart/form-data,
    создаёт и возвращает экземпляр файла.

    Параметры
    ----------
    issue_id : int
        Идентификатор запроса, в рамках которого сохраняется файл.

    file_data : cherrypy._cpreqbody.Part
        Данные файла.

    Возвращает
    ----------
    file : File
        Экземпляр сохранённого файла.
    '''
    if not file_data or not file_data.file:
        return None

    file_id = str(uuid.uuid4())
    file_path = f'issue_{issue_id}'
    project_manager().file_manager.save_uploaded_file(
        file_path,
        file_id,
        file_data.file.read())

    # Получение имени файла и преобразование в utf8 для корректной
    # работы с кириллицей
    if 'Content-Disposition' in file_data.headers:
        file_name = value_from_string(
            file_data.headers['Content-Disposition'],
            "filename",
        ).encode("latin1").decode("utf8")
    else:
        file_name = file_data.filename

    return File(file_name, file_id, file_path)
